import time

"""
Why customize the basic type instead of using struct parsing:
    Reason: Custom __str__
    Bad: use .value not itself
"""


class Base:
    """little endian type"""

    # parse str with int type, like: 0 as "false", 1 as "true"
    codes = None

    # parse str with byte type, like: 0x01 as "read", 0x02 as "write", 0x03 = read | write
    bytes = None

    def __init__(self, bytes, byteorder) -> None:
        assert (bytes is None) or (len(bytes) >= self.length), "wrong length"
        self._hex_str = None
        self.byteorder = byteorder

        if bytes is not None:
            self.value = int.from_bytes(bytes, byteorder)

    @property
    def hex_str(self):
        """02x % self.value"""
        if self._hex_str is None:
            if self.length == 1:
                self._hex_str = format(self.value, "02x")
            else:
                hexs = []
                val = self.value
                for _ in range(0, self.length):
                    hexs.append(format(val & 0xFF, f"02x"))
                    val >>= 8

                    self._hex_str = " ".join(hexs[::-1])
 
        return self._hex_str

    def __str__(self) -> str:
        pre = ""

        if self.codes is not None:
            if self.value <= len(self.codes):
                return self.codes[self.value]
            else:
                pre = "unknown "
        elif self.bytes is not None:
            bit = []
            for i in range(0, len(self.bytes)):
                if self.value & (1 << i):
                    bit.append(
                        self.bytes[i] if self.bytes[i] is not None else "unknown"
                    )
            if len(bit) > 0:
                return " | ".join(bit)
            else:
                return "None"

        return pre + self.hex_str + " | " + str(self.value)

    def __add__(self, other):
        if isinstance(other, int):
            return self.value + other
        elif isinstance(other, Base):
            return self.value + other.value
        raise NotImplementedError()

    def __radd__(self, other):
        if isinstance(other, int):
            return self.value + other
        raise NotImplementedError()

    def __sub__(self, other):
        if isinstance(other, int):
            return self.value - other
        raise NotImplementedError()

    def __rsub__(self, other):
        if isinstance(other, int):
            return other - self.value
        raise NotImplementedError()

    def __lshift__(self, other):
        if isinstance(other, int):
            return self.value << other
        raise NotImplementedError()

    def __rshift__(self, other):
        if isinstance(other, int):
            return self.value >> other
        raise NotImplementedError()

    def __truediv__(self, other):
        if isinstance(other, int):
            return self.value / other
        raise NotImplementedError()

    def __rtruediv__(self, other):
        if isinstance(other, int):
            return other / self.value
        raise NotImplementedError()

    def __mul__(self, other):
        if isinstance(other, int):
            return self.value * other
        raise NotImplementedError()

    def __rmul__(self, other):
        if isinstance(other, int):
            return other * self.value
        raise NotImplementedError

    def __eq__(self, other):
        if isinstance(other, int):
            return self.value == other
        raise NotImplementedError()

    def __ne__(self, other):
        if isinstance(other, int):
            return self.value != other
        raise NotImplementedError()

    def __and__(self, other):
        if isinstance(other, int):
            return self.value & other
        raise NotImplementedError()

    def __or__(self, other):
        if isinstance(other, int):
            return self.value | other
        raise NotImplementedError()

    def __xor__(self, other):
        if isinstance(other, int):
            return self.value ^ other
        raise NotImplementedError()

    def __lt__(self, other):
        if isinstance(other, int):
            return self.value < other
        raise NotImplementedError()

    def __gt__(self, other):
        if isinstance(other, int):
            return self.value > other
        raise NotImplementedError()

    def __le__(self, other):
        if isinstance(other, int):
            return self.value <= other
        raise NotImplementedError()

    def __ge__(self, other):
        if isinstance(other, int):
            return self.value >= other
        raise NotImplementedError()

    def __floordiv__(self, other):
        if isinstance(other, int):
            return self.value // other
        raise NotImplementedError()

    def __rfloordiv__(self, other):
        if isinstance(other, int):
            return other // self.value
        raise NotImplementedError()

    def __mod__(self, other):
        if isinstance(other, int):
            return self.value % other
        raise NotImplementedError()

    def __rmod__(self, other):
        if isinstance(other, int):
            return other % self.value
        raise NotImplementedError()

    def has(self, target):
        if self.codes is not None:
            return self.codes.index(target) == self.value
        elif self.bytes is not None:
            return (1 << self.bytes.index(target)) & self.value != 0
        else:
            return False


class le8(Base):
    length = 1

    def __init__(self, bytes: bytes) -> None:
        super().__init__(bytes, "little")


class le16(Base):
    length = 2

    def __init__(self, bytes: bytes) -> None:
        super().__init__(bytes, "little")


class le32(Base):
    length = 4

    def __init__(self, bytes: bytes = None, lo: le16 = None, hi: le16 = None) -> None:
        super().__init__(bytes, "little")
        if bytes is None:
            self.value = lo.value + (hi.value << 16)


class le64(Base):
    length = 8

    def __init__(self, bytes: bytes = None, lo: le32 = None, hi: le32 = None) -> None:
        super().__init__(bytes, "little")
        if bytes is None:
            self.value = lo.value + (hi.value << 32)


class u8(Base):
    length = 1

    def __init__(self, bytes: bytes) -> None:
        super().__init__(bytes, "big")


class u16(Base):
    length = 2

    def __init__(self, bytes: bytes) -> None:
        super().__init__(bytes, "big")


class u32(Base):
    length = 4

    def __init__(self, bytes: bytes) -> None:
        super().__init__(bytes, "big")


class HexArea:
    """just like c struct"""

    extern_str = ()

    @classmethod
    @property
    def length(cls):
        return sum((1 if cls is str else cls.length) * cnt for cls, cnt, _ in cls.rules)

    def __init__(self, bytes) -> None:
        self.bytes = bytes
        self.offset = 0
        self.load_rules(self.rules)

    def load_rules(self, rules, offset=None, bytes=None):
        offset = (self.offset if bytes is None else 0) if offset is None else offset
        bytes = self.bytes if bytes is None else bytes

        for cls, cnt, name in rules:
            if cls is str:
                bstr = bytes[offset : offset + cnt].split(b"\x00", maxsplit=1)[0]
                if len(bstr) == 0:
                    val = ""
                else:
                    val = bstr.decode()
                setattr(self, name, val)
                offset += cnt
            elif cnt == 1:
                setattr(self, name, cls(bytes[offset : offset + cls.length]))
                offset += cls.length
            else:
                if cls in (le8, le16, le32, le64, u8, u16, u32):
                    setattr(
                        self,
                        name,
                        IntArray(cls, cnt, bytes[offset : offset + cnt * cls.length]),
                    )
                    offset += cnt * cls.length
                else:
                    setattr(
                        self,
                        name,
                        Array(
                            cls(
                                bytes[
                                    offset
                                    + cls.length * i : offset
                                    + cls.length * (i + 1)
                                ]
                            )
                            for i in range(0, cnt)
                        ),
                    )

        if bytes is self.bytes:
            self.offset = offset

    def __str__(self) -> str:
        rules_str = "\n".join(
            (name + ": " + str(getattr(self, name)).replace("\n", "\n\t"))
            for _, _, name in self.rules
        )
        if self.extern_str is None:
            return rules_str

        extern_str = "\n".join(
            (name + ": " + str(getattr(self, name)).replace("\n", "\n\t"))
            for name in self.extern_str if hasattr(self, name)
        )
        if len(extern_str) == 0:
            return rules_str
        else:
            return rules_str + "\n" + extern_str


class IntArray:
    """Array only has sample class which isinstance 'Base', __str__ with ' '"""

    def __init__(self, cls, cnt, bytes: bytes) -> None:
        assert len(bytes) >= cnt * cls.length, "wrong length"
        self.value = [
            cls(bytes[cls.length * i : cls.length * (i + 1)]) for i in range(0, cnt)
        ]
        self.length = cnt * cls.length

    def __str__(self) -> str:
        return " ".join([cls.hex_str for cls in self.value])


class Array(list):
    """__str__ with '\\n'"""

    def __str__(self) -> str:
        """array with \t pre"""
        return "\n".join("\t" + str(self[i]).replace("\n", "\n\t") for i in range(len(self)))


class Linux2(HexArea):
    length = 12

    rules = (
        (le16, 1, "blocks_high"),
        (le16, 1, "file_acl_high"),
        (le16, 1, "uid_high"),
        (le16, 1, "gid_high"),
        (le16, 1, "checksum_lo"),
        (le16, 1, "reserved"),
    )


class Hurd2(HexArea):
    length = 12

    rules = (
        (le16, 1, "reserved1"),
        (u16, 1, "mode_high"),
        (u16, 1, "uid_high"),
        (u16, 1, "gid_high"),
        (u32, 1, "author"),
    )


class Masix2(HexArea):
    length = 8

    rules = (
        (le16, 1, "reserved1"),
        (le16, 1, "file_acl_high"),
        (u32, 2, "reserved2"),
    )


class Union:
    rules = None

    def __init__(self, bytes, choose: str = None) -> None:
        self.choose = choose

        for cls, name in self.rules:
            setattr(self, name, cls(bytes))

    def __str__(self) -> str:
        if self.choose is None:
            strs = []
            for _, name in self.rules:
                strs.append(
                    name + ": " + str(getattr(self, name)).replace("\n", "\n\t")
                )
            return "\n".join(strs)
        else:
            return (
                self.choose
                + ": "
                + str(getattr(self, self.choose)).replace("\n", "\n\t")
            )


class Inode_Osd1(Union):
    length = 4

    rules = (
        (le32, "linux1_version"),
        (u32, "hurd1_translator"),
        (u32, "masix1_reserved1"),
    )

    def __init__(self, bytes, choose: str = None) -> None:
        super().__init__(bytes, choose if choose is not None else "linux1_version")


class Inode_Osd2(Union):
    length = 12

    rules = (
        (Linux2, "linux2"),
        (Hurd2, "hurd2"),
        (Masix2, "masix2"),
    )

    def __init__(self, bytes, choose: str = None) -> None:
        super().__init__(bytes, choose if choose is not None else "linux2")


class SuperBlock_Checksum_Type(le8):
    # Metadata checksum algorithm codes
    codes = ["None", "EXT4_CRC32C_CHKSUM"]


class SuperBlock_Flags(le32):
    bytes = [
        "SIGNED_HASH",
        "UNSIGNED_HASH",
        "TEST_FILESYS",  # to test development code
    ]


class SuperBlock_Feature_Compat(le32):
    bytes = [
        "DIR_PREALLOC",
        "IMAGIC_INODES",
        "HAS_JOURNAL",
        "EXT_ATTR",
        "RESIZE_INODE",
        "DIR_INDEX",
        None,
        None,
        None,
        "SPARSE_SUPER2",
        "FAST_COMMIT",
        "STABLE_INODES",
    ]


class SuperBlock_Feature_Incompat(le32):
    bytes = [
        "COMPRESSION",
        "FILETYPE",
        "RECOVER",
        "JOURNAL_DEV",
        "META_BG",
        None,
        "EXTENTS",
        "64BIT",
        "MMP",
        "FLEX_BG",
        "EA_INODE",
        None,
        "DIRDATA",
        "CSUM_SEED",
        "LARGEDIR",
        "INLINE_DATA",
        "ENCRYPT",
        "CASEFOLD",
    ]


class SuperBlock_Feature_RO_Compat(le32):
    bytes = [
        "SPARSE_SUPER",
        "LARGE_FILE",
        "BTREE_DIR",
        "HUGE_FILE",
        "GDT_CSUM",
        "DIR_NLINK",
        "EXTRA_ISIZE",
        None,
        "QUOTA",
        "BIGALLOC",
        "METADATA_CSUM",
        None,
        "READONLY",
        "PROJECT",
        None,
        "VERITY",
    ]


class MMP(HexArea):
    """Multiple Mount Protection"""

    rules = (
        (le32, 1, "magic"),
        (le32, 1, "seq"),

        (le64, 1, "time"),
        (str, 64, "nodename"),
        (str, 32, "bdevname"),

        (le16, 1, "check_interval"),

        (le16, 1, "pad1"),
        (le32, 226, "pad2"),
        (le32, 1, "checksum"),
    )


class InodeMode(le16):
    special_bytes = {
        0o0140000: "SOCK",
        0o0120000: "LNK",
        0o0100000: "REG",
        0o0060000: "BLK",
        0o0040000: "DIR",
        0o0020000: "CHR",
        0o0010000: "FIFO",
    }
    bytes = {
        0o0001000: "SVTX",
        0o0002000: "SUID",
        0o0004000: "SGID",
    }

    def __str__(self) -> str:
        strs = []
        value = self.value & 0o00170000
        for byte, name in self.special_bytes.items():
            if byte & value == byte:
                strs.append(name)

        value = self.value & 0o00170000
        for byte, name in self.bytes.items():
            if byte & self.value != 0:
                strs.append(name)

            return " | ".join(strs)

    def isdir(self):
        return (self.value & 0o00170000) & 0o0040000 == 0o0040000

    def islnk(self):
        return (self.value & 0o00170000) & 0o0120000 == 0o0120000


class Timestamp(le32):
    """epoch in Timestamp_Extra, not implemented yet"""

    def __str__(self) -> str:
        return (
            super().__str__()
            + " | "
            + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.value))
        )


class Timestamp_Extra(le32):
    """(nsec << 2 | epoch)"""

    def __str__(self) -> str:
        nsec = self.value >> 2
        epoch = self.value & 0x3

        return super().__str__() + " | epoch: %d, ns: %09d" % (epoch, nsec)


class InodeFlag(le32):
    bytes = [
        "SECRM",  # Secure deletion
        "UNRM",  # Undelete
        "COMPR",  # Compress file
        "SYNC",  # Synchronous updates

        "IMMUTABLE",  # Immutable file
        "APPEND",  # writes to file may only append
        "NODUMP",  # do not dump file
        "NOATIME",  # do not update atime

        "DIRTY",
        "COMPRBLK",  # One or more compressed clusters
        "NOCOMPR",  # Don't compress
        "ENCRYPT",  # Encrypted file

        "INDEX",  # hash-indexed directory
        "IMAGI",  # AFS directory
        "JOURNAL_DATA",  # file data should be journaled
        "NOTAIL",  # file tail should not be merged
        "DIRSYNC",  # dirsync behaviour (directories only)
        "TOPDIR",  # Top of directory hierarchies*/
        "HUGE_FILE",  # Set to each huge file
        "EXTENTS",  # Inode uses extents
        "VERITY",  # Verity protected inode
        "EA_INODE",  # Inode used for large EA
        None,  # 22 was formerly EXT4_INODE_EOFBLOCKS
        None,
        None,
        "DAX",  # Inode is DAX
        None,
        None,
        "INLINE_DATA",  # Data in inode.
        "PROJINHERIT",  # Create with parents projid
        "CASEFOLD",  # Casefolded directory
        "RESERVED",  # reserved for ext4 lib
    ]


class InodeExtent(HexArea):
    rules = (
        (le32, 1, "block"),
        (le16, 1, "len"),
        (le16, 1, "start_hi"),
        (le32, 1, "start_lo"),
    )

    extern_str = ("start",)

    def __init__(self, bytes) -> None:
        super().__init__(bytes)
        self.start = le64(lo=self.start_lo, hi=self.start_hi)

        # ref: ext4_ext_get_actual_len
        self.len = self.len if (self.len <= (1 << 15)) else (self.len - (1 << 15))

    def hex_str(self):
        return super().__str__()


class InodeExtentIdx(HexArea):
    rules = (
        (le32, 1, "block"),
        (le32, 1, "leaf_lo"),
        (le16, 1, "leaf_hi"),
        (u16, 1, "unused"),
    )

    def __init__(self, bytes) -> None:
        super().__init__(bytes)
        self.leaf = le64(lo=self.leaf_lo, hi=self.leaf_hi)


class InodeExtentHeader(HexArea):
    rules = (
        (le16, 1, "magic"),
        (le16, 1, "entries"),
        (le16, 1, "max"),
        (le16, 1, "depth"),
        (le32, 1, "generation"),
    )

    MAX_EXTENT_DEPTH = 5


class InodeFastLNK():
    _length = 60

    def __init__(self, bytes) -> None:
        self.value = bytes

    @property
    def length(self):
        return self._length

    @length.setter
    def length(self, val):
        self._length = val
        self.value = self.value[:self._length]

    def __str__(self) -> str:
        if self._length == type(self).length:
            return self.value
        else:
            return self.value.decode()


class InodeBlock(Union):
    length = 60

    rules = (
        (InodeExtentHeader, "extent"),
        (InodeFastLNK, "link")
        )


class FileType(u8):
    codes = [
        "UNKNOWN",
        "REG",
        "DIR",
        "CHR",
        "BLK",
        "FIFO",
        "SOCK",
        "LNK",
    ]
